package com.xucia.jsponic.datasource;


import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;

import com.xucia.jsponic.data.DataSourceHelper;
import com.xucia.jsponic.data.ObjectId;
import com.xucia.jsponic.data.PersistableList;
import com.xucia.jsponic.data.LazyPropertyId;
import com.xucia.jsponic.remote.ClientConnection;
import com.xucia.jsponic.remote.JsponSender;
import com.xucia.jsponic.remote.ClientConnection.IndividualRequest;
@Deprecated
public class HttpJsponSource extends AbstractJsponSource implements WritableDataSource{
	MultiThreadedHttpConnectionManager connectionManager = 
  		new MultiThreadedHttpConnectionManager();
  	HttpClient client = new HttpClient(connectionManager);
  	//TODO: need an individual connection for each host
  	IndividualRequest serverRequest = new ClientConnection().getIndividualRequest(null);
	@Override
	protected String getJsponString(String objectId) throws HttpException, IOException,JSONException{
		if (!objectId.toLowerCase().startsWith("http:/"))
			objectId = "http:/" + objectId;
		return makeRequest(new GetMethod(objectId));
	}
	private Map<String,Integer> unassignedAccessLevels = new HashMap<String,Integer>();
	/**
	 * Make a HTTP request to the server, this can be a GET or a POST. This does RPC handling associated with requests 
	 * @param method
	 * @return
	 * @throws HttpException
	 * @throws IOException
	 * @throws JSONException
	 */
	private String makeRequest(HttpMethod method) throws HttpException, IOException,JSONException {
		//client.setConnectionTimeout(1);
		IndividualRequest clientRequest = ClientConnection.getCurrentObjectResponse();
		ClientConnection.registerThisConnection(serverRequest);
    	String rpcs = ClientConnection.getCurrentObjectResponse().getWaitingRPCs();
    	if (rpcs.length() > 2)
    		method.setRequestHeader("RPCs", rpcs);
		try {
			int statusCode = client.executeMethod(method);
			Header rpcHeader = method.getResponseHeader("RPCs");
			if (rpcHeader != null)
				handleRPCs(rpcHeader.getValue());
			Header accessLevelHeader = method.getResponseHeader("Access-Level");
			if (accessLevelHeader != null) 
				putThreadValue(ACCESS_LEVEL_MAP_KEY,new JSONObject(accessLevelHeader.getValue()));
			
			if (statusCode != HttpStatus.SC_OK) 
				return "";
			byte[] responseBody = method.getResponseBody();
			return new String(responseBody);
		}
		finally {
			ClientConnection.registerThisConnection(clientRequest);
			method.releaseConnection();
		}
		
	}
	protected void putJspon(String objectId, JSONObject putObject) throws HttpException, IOException,JSONException{
		if (!objectId.toLowerCase().startsWith("http:/"))
			objectId = "http:/" + objectId;
		PostMethod method = new PostMethod(objectId);
		NameValuePair[] data = {
				new NameValuePair("Put",putObject.toString())
		};
		method.setRequestBody(data);
		convertJsponStringToObject(makeRequest(method));		
	}

	public void initParameters(JSONObject parameters) {
	}
	public void mapObject(PersistableInitializer initializer, String objectId) throws Exception {
		/*
		Object importMaps = DataSourceManager.getRootObject().get(GlobalData.IMPORT_MAPS);
		if (importMaps instanceof DataObject) {
			String prefix = id.substring(0,id.lastIndexOf('/'));
			Object mapping = ((DataObject)importMaps).get(prefix);
			if (mapping instanceof DataObject)
				mapping = ((DataObject)mapping).get(id.substring(prefix.length()));
			if (mapping instanceof DataObject)
				return (Identifiable) mapping;
		}
		return (Identifiable) wrapper(id,HttpJSPONObject.class);
*/
		Context.enter().putThreadLocal(ACCESS_LEVEL_MAP_KEY, new JSONObject()); // clear out the access level definitions
		int lastSlashIndex = objectId.lastIndexOf('/');
		if (lastSlashIndex == -1)
			Context.enter().putThreadLocal(SOURCE_URL_KEY,"");
		else
			Context.enter().putThreadLocal(SOURCE_URL_KEY,getId() + '/'+ objectId.substring(0,lastSlashIndex+1));
		Object value = new JSONTokener(getJsponString(objectId)).nextValue();
		if (value instanceof JSONArray)
			listFromJSPONArray((JSONArray) value);
		else if (value instanceof JSONObject)
			idFromJSPONObject((JSONObject) value);
		initializer.finished();
	}
	public Object getFieldValue(LazyPropertyId valueId) throws Exception {
		return convertJsponStringToObject(getJsponString(valueId.toString()));		
	}

	String JSPON_URL_TOKEN = "/jspon/";
	protected static void putThreadValue(Object key, Object value) {
		Context.enter().putThreadLocal(key,value);
	}
	protected static Object getThreadValue(Object key) {
		return Context.enter().getThreadLocal(key);		
	}
	@Override
	public ObjectId idFromJSPONObject(JSONObject object) throws JSONException {
    	ObjectId targetId;
    	String key;
    	String sourceUrl = (String) getThreadValue(SOURCE_URL_KEY);
		Date changesSince=null;
		if (object.has("update") && object.get("update") instanceof JSONObject && object.getJSONObject("update").has("changesSince")) {
			String value = object.getJSONObject("update").getString("changesSince");
			changesSince = new Date(Long.parseLong(value.substring(1,value.length()-1))); // handle dates			
		}
    	if (object.has("id")) {
			targetId = ObjectId.idForString(sourceUrl + object.remove("id")); // TODO: Just get the string and do the initialization in array or method
    	}
    	else if (object.has("aliasId")) {
			targetId = ObjectId.idForString(sourceUrl + object.remove("aliasId"));
    	}
    	else if (!object.has("update"))
    		targetId = createInitialObject(object).getId();
    	else
    		targetId = null;
    	if (object.length() == 0 || (object.has("ready") && ((Integer) object.remove("ready")) == 0))
    		return targetId;
    	PersistableInitializer initializer = DataSourceHelper.initializeObject(targetId);
    	if (object.has("array")) {
    		Object arrayObject = object.remove("array");
    		if (arrayObject instanceof JSONArray) {
	    		final JSONArray array = (JSONArray) arrayObject;
	    		object.remove("array");
	    		initializer.initializeList(new Iterator() {
	    			int i = 0;
					public boolean hasNext() {
						return array.length() > i;
					}
	
					public Object next() {
						try {
							return idOrValueFromJSON(array.get(i++));
						} catch (JSONException e) {
							throw new RuntimeException(e);
						}
					}
	
					public void remove() {
						throw new UnsupportedOperationException();
						
					}
	    			
	    		});
    		}
    		else {
    			assert(Integer) ((JSONObject) arrayObject).get("ready") == 0;
    			return targetId;
    		}
    	} else if (object.has("function")) {
    		Object methodObject= object.get("function");
    		if (methodObject instanceof String) 
    			initializer.initializeFunction((String) methodObject,trustJavaScript);
    		else {
    			assert(Integer) ((JSONObject) methodObject).get("ready") == 0;
    			return targetId;
    		}
    	}
    	for (Iterator iter = object.keys(); iter.hasNext();) {
    		key = (String) iter.next();  // TODO: This needs to be limited to alteration lists, so we don't get a conflict with fields that start with c$.  This may need to be identified on the client side
    		Object value = object.get(key);
    		if (key.startsWith("client/")) // This is a client id alteration which needs be changed a 
    		{
    			key = ClientConnection.getCurrentObjectResponse().getConnection().clientSideObject(key,createInitialObject(object)).getId().toString();
    		}
    		if ("update".equals(key)) {
    			//if ("delete".equals(value))
    				//return ERASURE_ENTITY;
    			if (value instanceof JSONObject)
    				updateList((PersistableList) targetId.getTarget(),(JSONObject) value);
    		}
    		else {
    			value = idOrValueFromJSON(value);
    			if (value == NOT_READY_FIELD)
    				value = new LazyPropertyId(targetId,key);
/*	    		if (key.equals(FUNCTION_CODE_KEY)) {
	    			value = functionCompression((String) value);
	    			}
*/	    		if ("array".equals(key)) {
				}
	    		else if (targetId != null) {
	    			try {
		    			if (value == getErasureEntity())
		    				initializer.setProperty(key, Scriptable.NOT_FOUND);
		    			else 
		    				initializer.setProperty(key, value);
		    			
	    			}
	    			catch (Exception e) {
	    				e.printStackTrace();
	    			}
	    		}
    		}
    	}
    	JSONObject accessLevels = (JSONObject) getThreadValue(ACCESS_LEVEL_MAP_KEY);
    	if (accessLevels != null) {
    		Object accessObject = accessLevels.opt(targetId.toString().substring(sourceUrl.length()));
    		if (accessObject == null)
    			accessObject = accessLevels.opt("default");
    		//Logger.getLogger(HttpJsponSource.class.toString()).info("client access " + accessObject);
//    		initializer.setAcl();
    	}
		initializer.finished();
/*		if (parentValue != null) // we will do it at the end so that if it is an append list entry it can be done without security problems
			target.set(GlobalData.PARENT_FIELD, parentValue);*/
    	return targetId;
	}
	class JsponNewObjectPersister implements NewObjectPersister {
		public Object getAcl() {
			return null;
		}

		ObjectId id;
		JSONObject object;
		JsponNewObjectPersister(ObjectId id,JSONObject object) {
			this.id = id;
			this.object = object;
		}
		public void finished() throws Exception {
		}

		public String getObjectId() {
			return id.getSubObjectId();
		}

		public WritableDataSource getSource() {
			return (WritableDataSource) id.getSource();
		}

		public void initializeAsFunction(String functionBody) throws Exception {
			object.put("function", functionBody);
		}

		public void initializeAsList(List values) throws Exception {
			JSONArray array = new JSONArray();
			object.put("array",array);
			for (Object value : values)
				array.put(recordValueAsJSON(value));
		}

		public void recordProperty(String name, Object value) throws Exception {
			object.put(name, recordValueAsJSON(value));
		}

		public void start() throws Exception {
		}
		
	}
	Object recordValueAsJSON(Object value) throws Exception {
		if (value instanceof ObjectId) {
			JSONObject valueJSON = new JSONObject();
        	((ObjectId)value).persistIfNeeded(new JsponNewObjectPersister(((ObjectId)value),valueJSON));
        	valueJSON.put("id", ((ObjectId)value).getSubObjectId());
        	return valueJSON;
		}
		if (value instanceof Date) {
			value = "\"@" + ((Date) value).getTime() + "@\"";
		}
		else if (value == null)
			value = JSONObject.NULL;
		return value;
	}
	JSONObject setupTransferObject(String objectId) throws JSONException {
		JSONObject putObject = new JSONObject();
		putObject.put("id",objectId);
		JSONObject updateObject = new JSONObject();
		updateObject.put("changesSince", JsponSender.objectToString(new Date()));
		putObject.put("update",updateObject);
		return putObject;
	}
	public void recordPropertyChange(String objectId, String name, Object value) throws Exception {
		JSONObject transferObject = setupTransferObject(objectId);
		transferObject.put(name, recordValueAsJSON(value));
		putJspon(objectId, transferObject);
	}
	public void recordPropertyAddition(String objectId, String name, Object value) throws Exception {
		recordPropertyChange(objectId, name, value);
	}
	
	public void recordPropertyRemoval(String objectId, String name) throws Exception {
		JSONObject transferObject = setupTransferObject(objectId);
		JSONObject valueJSON = new JSONObject();
		valueJSON.put("update","delete");
		transferObject.put(name, valueJSON);
		putJspon(objectId, transferObject);		
	}	
	public void recordListAdd(String objectId, Object value) throws Exception {
		JSONObject transferObject = setupTransferObject(objectId);
		JSONObject updater= transferObject.getJSONObject("update");
		JSONArray appends = new JSONArray();
		updater.put("append",appends);
		appends.put(recordValueAsJSON(value));
		putJspon(objectId, transferObject);		
	}

	public void recordListRemoval(String objectId, Object value) throws Exception {
		JSONObject transferObject = setupTransferObject(objectId);
		JSONObject updater= transferObject.getJSONObject("update");
		JSONArray deletes = new JSONArray();
		updater.put("delete",deletes);
		deletes.put(recordValueAsJSON(value));
		putJspon(objectId, transferObject);		
	}

}
